菜鸟学飞之frida整合怪
前言 • 学习历程
1、阅读书籍
2、论坛
3、培训
4、开源项目
5、个人博客
6、练手和实战
序言 • fridaUiTools整合怪
整合工具开发,我觉得是一种比较高效的学习方式。将别人优秀的项目魔改,并且进行一定的拼合,整理成一个成套的工具。在整理的过程中,必然要熟悉对方的代码,并且对部分代码进行调整。在这个过程中,就能快速汲取到他人的经验。这种行为。就是所谓的整合怪/缝合怪了。
1、js脚本的hook和管理(对批量多个脚本同时hook,可以自定义脚本进行管理,可以保存加载。必须是在附加前进行操作。)
整合r0capture 整合jnitrace 整合ZenTracer java层的加解密相关自吐 ssl证书导出 ssl pining(整合DroidSSLUnpinning) 模糊匹配函数进行批量hook(整合ZenTracer) 模糊匹配so函数批量hook(参数统一方式打印,所以输出只能做参考) native的sub函数批量hook(参数统一方式打印,所以输出只能做参考) stalker的trace 脱壳相关(整合frida_dump、FRIDA-DEXDump、fart) 自定义脚本添加 (todo 待开发) patch汇编代码 (todo 待开发)
2、常用功能的调用(常见的内存漫游操作进行功能化,以后再根据实战需求增加新功能。必须是在附加后进行操作。)
fart主动调用 DUMPDex主动调用 dump打印指定地址 dump指定模块 wallBreak整合
3、初始化信息
附加进程成功后,将一些常用的信息在界面展示。目前只处理了module列表和class列表,以后再根据需求增加新的信息。
github:https://github.com/dqzg12300/fridaUiTools
一 • ZenTracer
github:https://github.com/hluwa/ZenTracer
功能:
界面化的批量hook多个类,可以通过拉黑过滤掉一些调用率特别高的类。
实现原理:
在js脚本中使用占位符{MATCHREGEX}和{BLACKREGEX}在后续替换,来传递参数需要批量trace的类名以及黑名单。遍历所有类匹配出符合要求的类名。并且不在黑名单中,则进行批量hook。批量hook时会将函数所有重载都hook上。最后是三种输出方式,正常的log输出、函数进入时的参数输出、函数结束时的返回值输出。
核心代码(简略):
//批量hook
function traceClass(clsname) {
try {
var target = Java.use(clsname);
//获取本类所有函数(注意getMethods这个是获取本类和父类中的函数。)
var methods = target.class.getDeclaredMethods();
methods.forEach(function (method) {
var methodName = method.getName();
//获取所有重载
var overloads = target[methodName].overloads;
overloads.forEach(function (overload) {
//参数的类型
var proto = "(";
overload.argumentTypes.forEach(function (type) {
proto += type.className + ", ";
});
if (proto.length > 1) {
proto = proto.substr(0, proto.length - 2);
}
proto += ")";
log("hooking: " + clsname + "." + methodName + proto);
//hook 函数
overload.implementation = function () {
var args = [];
var tid = getTid();
var tName = getTName();
for (var j = 0; j < arguments.length; j++) {
args[j] = arguments[j] + ""
}
//函数进入时的参数啥的在里面通过send传给py
enter(tid, tName, clsname, methodName + proto, args);
var retval = this[methodName].apply(this, arguments);
//函数结束时的返回值在里面通过send传给py
exit(tid, "" + retval);
return retval;
}
});
});
} catch (e) {
log("'" + clsname + "' hook fail: " + e)
}
}
//匹配符合要求的类。并且不在黑名单中的
if (Java.available) {
Java.perform(function () {
log('ZenTracer Start...');
//在js被读取时,会替换这里的数据
var matchRegEx = {MATCHREGEX};
var blackRegEx = {BLACKREGEX};
Java.enumerateLoadedClasses({
onMatch: function (aClass) {
for (var index in matchRegEx) {
// console.log(matchRegEx[index]);
if (match(matchRegEx[index], aClass)) {
var is_black = false;
for (var i in blackRegEx) {
if (match(blackRegEx[i], aClass)) {
is_black = true;
log(aClass + "' black by '" + blackRegEx[i] + "'");
break;
}
}
if (is_black) {
break;
}
log(aClass + "' match by '" + matchRegEx[index] + "'");
traceClass(aClass);
}
}
},
onComplete: function () {
log("Complete.");
}
});
});
}
改造并整合:
优化界面显示,优化类名的输入环节。每次附加进程后,都将所有类名都保存下来,这里就可以选择之前缓存下来的所有类数据。然后根据输入智能过滤,可以快捷方便的找到自己想要hook的类。快捷添加操作,将经常要hook的类放在里面,就可以迅速的hook了。
相关贴图:
二 • r0capture
github:https://github.com/r0ysue/r0capture
功能:
安卓应用抓包的通杀脚本,并且可以过证书校验解绑定ssl pining,可以导出客户端ssl证书,可以导出pcap文件。
实现原理:
核心代码(简略):
//导出证书到指定路径,并使用新密码
function storeP12(pri, p7, p12Path, p12Password) {
var X509Certificate = Java.use("java.security.cert.X509Certificate")
var p7X509 = Java.cast(p7, X509Certificate);
var chain = Java.array("java.security.cert.X509Certificate", [p7X509])
var ks = Java.use("java.security.KeyStore").getInstance("PKCS12", "BC");
ks.load(null, null);
ks.setKeyEntry("client", pri, Java.use('java.lang.String').$new(p12Password).toCharArray(), chain);
try {
var out = Java.use("java.io.FileOutputStream").$new(p12Path);
ks.store(out, Java.use('java.lang.String').$new(p12Password).toCharArray())
} catch (exp) {
console.log(exp)
}
}
//在服务器校验客户端的情形下,帮助dump客户端证书,并保存为p12的格式,证书密码为r0ysue
Java.use("java.security.KeyStore$PrivateKeyEntry").getPrivateKey.implementation = function () {
var result = this.getPrivateKey()
var packageName = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext().getPackageName();
storeP12(this.getPrivateKey(), this.getCertificate(), '/sdcard/Download/' + packageName + uuid(10, 16) + '.p12', 'r0ysue');
var message = {};
message["function"] = "dumpClinetCertificate=>" + '/sdcard/Download/' + packageName + uuid(10, 16) + '.p12' + ' pwd: r0ysue';
message["stack"] = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new());
var data = Memory.alloc(1);
send(message, Memory.readByteArray(data, 1))
return result;
}
//SSLpinning helper 帮助定位证书绑定的关键代码
Java.use("java.io.File").$init.overload('java.io.File', 'java.lang.String').implementation = function (file, cert) {
var result = this.$init(file, cert)
//打印堆栈
var stack = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new());
//匹配证书绑定的函数是否在调用链中
if (file.getPath().indexOf("cacert") >= 0 && stack.indexOf("X509TrustManagerExtensions.checkServerTrusted") >= 0) {
var message = {};
message["function"] = "SSLpinning position locator => " + file.getPath() + " " + cert;
message["stack"] = stack;
var data = Memory.alloc(1);
send(message, Memory.readByteArray(data, 1))
}
return result;
}
def log_pcap(pcap_file, ssl_session_id, function, src_addr, src_port,
dst_addr, dst_port, data):
"""Writes the captured data to a pcap file.
Args:
pcap_file: The opened pcap file.
ssl_session_id: The SSL session ID for the communication.
function: The function that was intercepted ("SSL_read" or "SSL_write").
src_addr: The source address of the logged packet.
src_port: The source port of the logged packet.
dst_addr: The destination address of the logged packet.
dst_port: The destination port of the logged packet.
data: The decrypted packet data.
"""
t = time.time()
if ssl_session_id not in ssl_sessions:
ssl_sessions[ssl_session_id] = (random.randint(0, 0xFFFFFFFF),
random.randint(0, 0xFFFFFFFF))
client_sent, server_sent = ssl_sessions[ssl_session_id]
if function == "SSL_read":
seq, ack = (server_sent, client_sent)
else:
seq, ack = (client_sent, server_sent)
for writes in (
# PCAP record (packet) header
("=I", int(t)), # Timestamp seconds
("=I", int((t * 1000000) % 1000000)), # Timestamp microseconds
("=I", 40 + len(data)), # Number of octets saved
("=i", 40 + len(data)), # Actual length of packet
# IPv4 header
(">B", 0x45), # Version and Header Length
(">B", 0), # Type of Service
(">H", 40 + len(data)), # Total Length
(">H", 0), # Identification
(">H", 0x4000), # Flags and Fragment Offset
(">B", 0xFF), # Time to Live
(">B", 6), # Protocol
(">H", 0), # Header Checksum
(">I", src_addr), # Source Address
(">I", dst_addr), # Destination Address
# TCP header
(">H", src_port), # Source Port
(">H", dst_port), # Destination Port
(">I", seq), # Sequence Number
(">I", ack), # Acknowledgment Number
(">H", 0x5018), # Header Length and Flags
(">H", 0xFFFF), # Window Size
(">H", 0), # Checksum
(">H", 0)): # Urgent Pointer
pcap_file.write(struct.pack(writes[0], writes[1]))
pcap_file.write(data)
if function == "SSL_read":
server_sent += len(data)
else:
client_sent += len(data)
ssl_sessions[ssl_session_id] = (client_sent, server_sent)
我去掉了pcap的保存,直接调用脚本,把输出方式统一起来(去掉所有js的console.log打印,统一格式send到py进行输出)。其他功能都保持原有的。
三 • jnitrace/JNI-Frida-Hook
github:https://github.com/Areizen/JNI-Frida-Hook
功能:
实现原理:
核心代码(简略):
//在要hook的模块加载完后,才调用hook代码
Interceptor.attach(Module.findExportByName(null, 'android_dlopen_ext'),{
onEnter: function(args){
// first arg is the path to the library loaded
var library_path = Memory.readCString(args[0])
//判断当前加载的模块是否是目标模块
if( library_path.includes(library_name)){
console.log("[...] Loading library : " + library_path)
library_loaded = 1
}
},
onLeave: function(args){
// if it's the library we want to hook, hooking it
if(library_loaded == 1){
console.log("[+] Loaded")
//hook目标函数
hook_jni(library_name, function_name)
library_loaded = 0
}
}
})
/*
Calculate the given funcName address from the JNIEnv pointer //计算出jni函数的地址
*/
function getJNIFunctionAdress(jnienv_addr,func_name){
//最关键的起始就是这里,根据jnienv的地址和函数名,计算出偏移,其实就是拿函数的当前索引。这个了解类对象的结构就很清楚了。
var offset = jni_struct_array.indexOf(func_name) * Process.pointerSize
// console.log("offset : 0x" + offset.toString(16))
return Memory.readPointer(jnienv_addr.add(offset))
}
// Hook all function to have an overview of the function called //hook全部jni函数
function hook_all(jnienv_addr){
jni_struct_array.forEach(function(func_name){
// Calculating the address of the function
if(!func_name.includes("reserved"))
{
var func_addr = getJNIFunctionAdress(jnienv_addr,func_name)
Interceptor.attach(func_addr,{
onEnter: function(args){
console.log("[+] Entered : " + func_name)
}
})
}
})
}
改造并整合:
他只针对了spawn的附加情况进行hook。我调整了下,判断是哪种附加,再进行不同方式的调用,最后统一下输出的方式。
四 • DroidSSLUnpinning
github:https://github.com/WooyunDota/DroidSSLUnpinning
功能:
主要是处理防抓包的双向验证的,客户端验证服务端的证书。这个项目可以解掉证书绑定,让中间人抓包正常运行。效果和JustTrustMe差不多。他厉害的地方在于支持各种库的解绑定,市面上大多数的绑定方式他都有处理到。下面列一下大佬的支持的库:
1.SSLcontext
2.okhttp
3.webview
4.XUtils
5.httpclientandroidlib
6.JSSE
7.network\_security\_config (android 7.0+)
8.Apache Http client (support partly)
9.OpenSSLSocketImpl
10.TrustKit
11.Cronet
实现原理:
其实实现不难,但是关键是你要熟悉各种库的正向解绑定,知道是哪个函数来绑定的,然后将绑定函数给替换掉,直接改成空函数。所以像他这种支持这么多的,就比较厉害了。
核心代码(简略):
//代码太长。这里只简单放两种解绑定的例子
//WebView的解绑定
var WebViewClient = Java.use("android.webkit.WebViewClient");
WebViewClient.onReceivedSslError.implementation = function(webView, sslErrorHandler, sslError) {
quiet_send("WebViewClient onReceivedSslError invoke");
//执行proceed方法
sslErrorHandler.proceed();
return;
};
WebViewClient.onReceivedError.overload('android.webkit.WebView', 'int', 'java.lang.String', 'java.lang.String').implementation = function(a, b, c, d) {
quiet_send("WebViewClient onReceivedError invoked");
return;
};
WebViewClient.onReceivedError.overload('android.webkit.WebView', 'android.webkit.WebResourceRequest', 'android.webkit.WebResourceError').implementation = function() {
quiet_send("WebViewClient onReceivedError invoked");
return;
};
//okhttp的解绑定
var OkHttpClient = Java.use("com.squareup.okhttp.OkHttpClient");
OkHttpClient.setCertificatePinner.implementation = function(certificatePinner) {
// do nothing
quiet_send("OkHttpClient.setCertificatePinner Called!");
return this;
};
// Invalidate the certificate pinnet checks (if "setCertificatePinner" was called before the previous invalidation)
var CertificatePinner = Java.use("com.squareup.okhttp.CertificatePinner");
CertificatePinner.check.overload('java.lang.String', '[Ljava.security.cert.Certificate;').implementation = function(p0, p1) {
// do nothing
quiet_send("okhttp Called! [Certificate]");
return;
};
CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function(p0, p1) {
// do nothing
quiet_send("okhttp Called! [List]");
return;
};
五 • stalker
github:https://github.com/bmax121/sktrace
功能:
这个项目主要是用stalker来实现trace汇编代码,打印每一句汇编指令执行后寄存器的变化。一般用于辅助分析算法还原。但是由于frida的stalker本身对arm32的支持不太好,所以这个项目目前还不支持arm32。目前还未支持spawn附加,对于c的打印方式还未完善,没有打印出寄存器的具体数值。
实现原理:
首先CModule声明了一段c的代码,然后transform设置使用c的函数。我想,他可能是为了方便打印数据。工作流程比较简单,就是设置了目标模块,设置了符号或地址(一般是函数开始的地址,会一直执行到这个函数完,所以不用设置终止位置)。
他设置了两种打印方式stalkerTraceRangeC和stalkerTraceRange。用c的打印方式结果展示的比较好,但是缺少寄存器数值变化。另一种则是直接发送到py,让py部分来处理结果,但是我看他py部分也是没有解析输出。自己动手改良了一下。
核心代码(简略):
function traceAddr(addr) {
let moduleMap = new ModuleMap();
let targetModule = moduleMap.find(addr);
console.log(JSON.stringify(targetModule))
let exports = targetModule.enumerateExports();
let symbols = targetModule.enumerateSymbols();
//先是hook要trace的位置
Interceptor.attach(addr, {
onEnter: function(args) {
this.tid = Process.getCurrentThreadId()
//这个trace方式是c打印的,下面那个是发送详细数据给py打印的。
//stalkerTraceRangeC(this.tid, targetModule.base, targetModule.size)
stalkerTraceRange(this.tid, targetModule.base, targetModule.size)
},
onLeave: function(ret) {
Stalker.unfollow(this.tid);
Stalker.garbageCollect()
send({
type: "fin",
tid: this.tid
})
}
})
}
//我准备使用发送到py详细数据的打印方式,不是很喜欢混c的语言来处理。感觉会容易出错
function stalkerTraceRange(tid, base, size) {
Stalker.follow(tid, {
transform: (iterator) => {
const instruction = iterator.next();
const startAddress = instruction.address;
const isModuleCode = startAddress.compare(base) >= 0 &&
startAddress.compare(base.add(size)) < 0;
// const isModuleCode = true;
//transform是每个block触发。这里每个block触发的时候遍历出所有指令。
do {
iterator.keep();
if (isModuleCode) {
//这里可以看到数据如果是inst就是一个指令,我们就需要解析打印
//输出样本如下
//'payload': {'type': 'inst', 'tid': 19019, 'block': '0x74fd8d4ff4', 'val': '{"address":"0x74fd8d4ffc","next":"0x4","size":4,"mnemonic":"add","opStr":"sp, sp, #0x70","operands":[{"type":"reg","value":"sp"},{"type":"reg","value":"sp"},{"type":"imm","value":"112"}],"regsRead":[],"regsWritten":[],"groups":[]}'}}
//py解析打印格式"add sp, sp, #0x70 //sp=112" 这里的处理应该还要更复杂。暂时先简单处理
send({
type: 'inst',
tid: tid,
block: startAddress,
val: JSON.stringify(instruction)
})
//这里是打印所有寄存器
//输出样本如下
//{'type': 'ctx', 'tid': 19019, 'val': '{"pc":"0x74fd8d4fe8","sp":"0x7fc28609d0","x0":"0x0","x1":"0x7fc2860908","x2":"0x0","x3":"0x756aec1349","x4":"0x7fc28608f0","x5":"0x14059dbe","x6":"0x7266206f6c6c6548","x7":"0x2b2b43206d6f7266","x8":"0x0","x9":"0x65af2e18847fd289","x10":"0x1","x11":"0x7fc2860a20","x12":"0xe","x13":"0x7fc2860a20","x14":"0xffffff0000000000","x15":"0x756aeed1b5","x16":"0x74fd8fadc8","x17":"0x74fd8d50d8","x18":"0x75f0bda000","x19":"0x75f02f9c00","x20":"0x756af59490","x21":"0x75f02f9c00","x22":"0x7fc2860c90","x23":"0x74ffcee337","x24":"0x4","x25":"0x75f04b4020","x26":"0x75f02f9cb0","x27":"0x1","x28":"0x756b3f2000","fp":"0x7fc2860a30","lr":"0x74fd8d4fdc"}'}}
//这里是寄存器变化时调用
iterator.putCallout((context) => {
send({
type: 'ctx',
tid: tid,
val: JSON.stringify(context)
})
})
}
} while (iterator.next() !== null);
}
})
}
改造并整合:
优化py结果打印,增加spawn支持。
六 • frida_hook_libart
github:https://github.com/lasting-yang/frida_hook_libart
功能:(ps:yang大神的三件套,向大佬学习,给大佬递茶。)
实现原理:
核心代码(简略):
//hook_RegisterNative.js的部分,就是这段。找RegisterNative函数的地址。
var symbols = Module.enumerateSymbolsSync("libart.so");
var addrRegisterNatives = null;
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
//_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi
if (symbol.name.indexOf("art") >= 0 &&
symbol.name.indexOf("JNI") >= 0 &&
symbol.name.indexOf("RegisterNatives") >= 0 &&
symbol.name.indexOf("CheckJNI") < 0) {
addrRegisterNatives = symbol.address;
log("RegisterNatives is at "+symbol.address+" "+symbol.name);
}
}
//hook_artmethod.js的部分
//这里是遍历所有符号,匹配出ArtMethod的Invoke
var module_libart = Process.findModuleByName("libart.so");
var symbols = module_libart.enumerateSymbols();
var ArtMethod_Invoke = null;
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
var address = symbol.address;
var name = symbol.name;
var indexArtMethod = name.indexOf("ArtMethod");
var indexInvoke = name.indexOf("Invoke");
var indexThread = name.indexOf("Thread");
if (indexArtMethod >= 0
&& indexInvoke >= 0
&& indexThread >= 0
&& indexArtMethod < indexInvoke
&& indexInvoke < indexThread) {
//将后面的hook代码去掉。可以看到这里最终匹配到的结果是_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc
//转换下格式之后的结果是art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)
console.log(name);
ArtMethod_Invoke = address;
}
}
//如果上面匹配到了Invoke函数后。就hook打印。
if (ArtMethod_Invoke) {
Interceptor.attach(ArtMethod_Invoke, {
onEnter: function (args) {
var method_name = prettyMethod(args[0], 0);
if (!(method_name.indexOf("java.") == 0 || method_name.indexOf("android.") == 0)) {
console.log("ArtMethod Invoke:" + method_name + ' called from:\n' +
Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join('\n') + '\n');
}
}
});
}
//这里也是个重点。打印当前函数名的方式。
function prettyMethod(method_id, withSignature) {
const result = new StdString();
Java.api['art::ArtMethod::PrettyMethod'](result, method_id, withSignature ? 1 : 0);
return result.disposeToString();
}
//hook_art.js的重点部分
//这个就不放了。一整块有点大。简单说下,就是遍历libart.so所有符号列表找到一些常用的jni函数地址。然后打印输出
改造并整合:
七 • frida_dump
github:https://github.com/lasting-yang/frida_dump
功能:
实现原理:
改造并整合:
测试了下libart.so不需要spawn判断,都可以获取到,所以去掉spawn判断部分。然后在dex保存的时候,碰到了权限问题。不知道是不是和安卓10有关,总之修改成py来负责创建目录,赋值权限,增加功能从手机直接把脱壳好的文件下载到项目内。dump_dex_class的dump_dex抽成功能单独调用。
八 • FRIDA-DEXDump
github:https://github.com/hluwa/FRIDA-DEXDump
功能:
也是脱壳,不过和上面的方式不一样,是在内存中检索dex的特征的,再dump出来进行脱壳。同时支持objection。
实现原理:
改造并整合:统一下日志输出
九 • fart
github:https://github.com/hanbinglengyue/FART
功能:
也是脱壳用的。基于主动调用的脱壳,可以过掉大多数函数抽取壳。这是fart的frida版本,免去了编译rom。
实现原理:
1、FART:ART环境下基于主动调用的自动化脱壳方案
2、FART正餐前甜点:ART下几个通用简单高效的dump内存中dex方法
3、拨云见日:安卓APP脱壳的本质以及如何快速发现ART下的脱壳点
改造并整理:
将fart主动调用和dumpclass主动调用设置到rpc中。在功能里面来调用触发,增加将libart.so快捷push到手机并chmod权限的功能,另外测试发现LoadMethod地址的获取处,在安卓10无法获取到函数地址,修改成支持安卓10的。代码如下:
var versionData="ClassDataItemIterator";
if(Java.androidVersion=="10"){
versionData="ClassAccessor";
}
var symbols = Module.enumerateSymbolsSync("libart.so");
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
//_ZN3art11ClassLinker10LoadMethodERKNS_7DexFileERKNS_21ClassDataItemIteratorENS_6HandleINS_6mirror5ClassEEEPNS_9ArtMethodE
if (symbol.name.indexOf("ClassLinker") >= 0
&& symbol.name.indexOf("LoadMethod") >= 0
&& symbol.name.indexOf("DexFile") >= 0
&& symbol.name.indexOf(versionData) >= 0
&& symbol.name.indexOf("ArtMethod") >= 0) {
addrLoadMethod = symbol.address;
break;
}
}
十 • Wallbreaker
github:https://github.com/hluwa/Wallbreaker
功能:
主要是内存漫游,搜索内存中的java类和对象,并且可以打印类的结构体,以及对象的数据。
实现原理:
改造并整理:
结果输出方式调整。rpc.exports修改初始化方式,以免覆盖到其他js的rpc函数。将需要使用的所有脚本添加后,最后默认追加这个脚本。由于我默认做了类列表和过滤功能,所以classsearch可能有点鸡肋。
相关贴图:
看雪ID:misskings
https://bbs.pediy.com/user-home-659397.htm
*本文由看雪论坛 misskings 原创,转载请注明来自看雪社区
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!